module ActsAsDynamic::SimpleNavigator
  module InstanceMethods

    def index
      get_list
      set_model model_class.new
      before_render
      respond_to do |format|
        format.html do
          erb = <<-end_erb
            <div id="#{model_name}_grid">
              <%= render :partial => 'list' %>
            </div>
          end_erb
          render :inline => erb, :layout => true
        end

        format.js do
          name = model_name
          render :update do |page|
            page["#{name}_grid"].replace_html :partial => 'list'
            page[:right].replace_html :partial => 'acciones' unless params[:search]
          end
        end

        format.xml { render :xml => @list.to_xml(:root => model_list_name.to_s) }

      end
    end
    
    def new
      set_model model_class.new
      before_render
      respond_to do |format|
        format.html do
          render :layout => true, :inline => <<-inline_src
            <% new_form_for do |f| %>
              <%= render :partial => 'form', :object => f %>
              <%= f.actions :delete => false %>
            <% end %>
          inline_src
        end
        
        format.js do 
          locals = get_inline_form_locals
          model = model_name
          render :update do |page|
            page.inline_blind_down(model, locals)
          end
        end
        
        format.xml { render :xml => @current.to_xml }
        
      end
    end

    def show
      before_render
      respond_to do |format|
        format.html do 
          render :layout => true, :inline => "<%= render :partial => 'show' %>"
        end
        format.js do
          render :update do |page|
            page[:content].replace_html :partial => 'show'
          end          
        end
        format.xml { render :xml => @current.to_xml }
      end
    end

    def create
      before_assign
      set_model model_class.new(params[model_name])
      if parent = options[:parent]
        @current.send("#{parent_key}=", @parent.id)
      end
      save_model
      @key = @success ? :success : :error
      @msg = @success ? "Registro creado." : "Error al crear el Registro."
      before_render
      respond_to do |format|
        format.html do
          flash[@key] = @msg
          if @success
            url = {:action => 'index'}
            url.merge! :"#{parent}_id" => @parent
            redirect_to url
          else
            render "#{@model_list_name}/new"
          end
        end
        
        format.js {update_record_ajax}
        
        format.xml do
          if @success
            head :created, :location => url_for(:id => @current)
          else
            head :bad_request
          end
        end
      end
    end

    def edit
      before_render
      respond_to do |format|
        format.html do
          render :layout => true, :inline => <<-inline_src
            <% edit_form_for do |f| %>
              <%= render :partial => 'form', :object => f %>
              <%= f.actions %>
            <% end %>
          inline_src
        end
        
        format.js do
          locals = get_inline_form_locals
          model = model_name
          render :update do |page|
            if params[:inline]
              page.inline_blind_down("#{model}", locals)
            else
              page.update_inline(:content, locals.merge(:options => {:header => nil}))
            end
          end
        end
        
        format.xml { render :xml => @current.to_xml }
        
      end
    end

    def update
      before_assign
      @current.attributes = params[model_name]
      save_model
      @key = @success ? :success : :error
      @msg = @success ? 'Datos actualizados.' : 'Error al actualizar los datos.'
      before_render
      respond_to do |format|
        format.html do
          flash[@key] = @msg
          if @success
            url = {:action => 'show', :id => @current}
            url.merge! :"#{parent}_id" => @parent if parent = options[:parent]
            redirect_to url
          else
            render "#{model_list_name}/edit"
          end
        end
        
        format.js {update_record_ajax}
        
        format.xml do
          if @success
            head :ok, :location => url_for(:id => @current)
          else
            head :bad_request
          end
        end
        
      end
    end

    def destroy
      @success = @current.destroy
      @key = @success ? :success : :error
      @msg = @success ? "Registro eliminado." : "Error al eliminar el registro."
      before_render
      respond_to do |format|
        format.html do
          flash[@key] = @msg
          if @success
            redirect_to :action => 'index'
          else
            redirect_to :action => 'edit', :id => @current
          end
        end
        
        format.js {update_record_ajax}
        
        format.xml do
          head @success ? :ok : :bad_request
        end
      end
    end

    #FIXME: Probablemente estos métodos deben ser métodos de clase.
    def my_url_for_hash(controller_name, action, element = nil, parent = nil, params = {})
      url = params.merge :controller => controller_name, :action => action
      url.merge! :id => element if element
      if parent
        parent_sym = (parent.to_s + '_id').to_sym
        url.merge! parent_sym => instance_variable_get('@' + parent.to_s)
      end
      url
    end
    
    def my_url_for(controller_name, action, element = nil, parent = nil, params = {})
      url_for(my_url_for_hash(controller_name, action, element, parent, params))
    end    

    def model_name
      self.class.instance_variable_get("@model_name")
    end
    
    def model_class
      self.class.instance_variable_get("@model_class")
    end
    
    def parent_class
      self.class.instance_variable_get("@parent_class")
    end
    
    protected

    def model_list_name
      model_name.to_s.pluralize.to_sym
    end
    
    def options
      self.class.options
    end

    def set_model(value)
      @current = instance_variable_set("@#{model_name}", value)
    end
    
    def set_model_list(value)
      @list = instance_variable_set("@#{model_list_name}", value)
    end
    
    def save_model
      begin
        model_class.transaction do 
          before_save
          if @success = @current.save!
            after_save
          end
        end 
      rescue ActiveRecord::RecordNotSaved, ActiveRecord::RecordInvalid
        logger.error("Ocurrió una exception al salvar el registro: " + $!)
        @success = false
      end
    end

    def get_inline_form_locals
      {:model => model_name, :current => @current, :parent => options[:parent], :options => {}}
    end
    
    def update_record_ajax
      get_list
      locals = get_inline_form_locals
      mname = model_name
      inline = "#{mname}_inline_form"
      render :update do |page|
        flash_text = show_flash(@key => @msg)
        page << "if ($('#{inline}')) {"
          if @success
            page.hide_inline(inline, flash_text, @current)
          else
            page.update_inline(inline, locals)
          end
        page << "} else {"
          page.replace_html :flash_area, flash_text
          if @success
            if ['destroy', 'create'].include?(controller.action_name)
              page["#{mname}_grid"].replace_html :partial => 'list'
            else
              page[:content].replace_html :partial => 'show'
            end
          else
            page[:content].replace_html :partial => '../../vendor/plugins/acts_as_dynamic/files/app/views/varias/inline_form',
               :locals => locals
          end
        page << "};"
        page.hide_flash(@key)
      end
    end
    
    def get_list
      conditions = combo_filter_conditions
      if conditions.empty?
        find_with_regular_filters
      else
        model_class.with_scope(:find => {:conditions => conditions}) do
          find_with_regular_filters
        end
      end
    end
    
    def combo_filter_conditions
      conditions = {}
      self.class.combo_filters.each do |filter|
        if value = params[filter.name]
          value = nil if value == '0'
          session[:"#{filter.name}_combo_filter"] = value
        end
        conditions[filter.name] = session[:"#{filter.name}_combo_filter"] if session[:"#{filter.name}_combo_filter"]
      end
      conditions
    end
    
    def find_with_regular_filters
      find_with_filter(:all) do 
        find_with_filter(current_filter) do
          get_list_find
        end
      end
    end
    
    def find_with_filter(filter_name)
      yield && return unless filter_name
      filter = self.class.get_filter(filter_name) 
      conditions = filter && filter.conditions
      model_class.with_scope(:find => {:conditions => conditions}) do 
        yield
      end
    end
    
    def current_filter
      filter = params[:filter] || session[:"#{model_name}_filter"]
      session[:"#{model_name}_filter"] = filter ? filter.to_sym : nil
      filter
    end
    
    def get_list_find
      block = Proc.new do 
        origin = if options[:parent]
          @parent.send(model_name.to_s.pluralize)
        else
          model_class
        end
        set_model_list origin.find(:all, :conditions => search_conditions, 
                                         :order => get_order, :include => options[:include])
      end
      self.respond_to?(:list_scope) ? list_scope(&block) : block.call
      apply_block_filters
      create_paginator
    end    
    
    # Agrega condiciones de búsqueda si se especificaron en los parámetros.
    def search_conditions
      conditions = nil
      if params[:search]
        fields = self.class.search_fields
        fields = fields.map {|field| "#{field} LIKE '%#{params[:search]}%'"}
        conditions = '(' + fields.join(' OR ') + ')'
      end
      conditions && !conditions.empty? ? conditions : nil
    end

    # Devuelve un string con formato SQL para ordenar los resultados.
    def get_order
      if field = get_sort_by
        field + get_order_param
      else
        if session[:"sort_by_#{model_name}"] && session[:"order_#{model_name}"]
          session[:"sort_by_#{model_name}"] + session[:"order_#{model_name}"]
        else
          nil
        end
      end
    end
    
    # Devuelve el campo por el cual se ordena.
    def get_sort_by
      if options[:order].nil?
        field = sort_by_without_defa
      else
        field = sort_by_field || options[:order].to_s
      end
      if model_class.content_columns.find{|c| c.name == field}
        session[:"sort_by_#{model_name}"] = field
      else
        field = nil
      end
      field
    end
    
    # Devuelve el campo por el cual se ordena cuando no hay uno asignado 
    # por defecto en el controller.
    def sort_by_without_defa
      if field = sort_by_field
        field
      else
        # Devuelve 'nombre' o el primer campo del modelo.
        #TODO: Debería ser el primer campo de la grilla?
        campos = model_class.columns.select {|c| c.name == 'nombre' }
        if !campos.blank?
          field = "nombre"
        else
          field = model_class.columns.first.name
        end
      end
      field
    end
    
    def sort_by_field
      field = params[:sort_by]
      field = session[:"sort_by_#{model_name}"] if !field && session[:"sort_by_#{model_name}"]
      field
    end
    
    def get_order_param
      order = ''
      if params[:order]
        order = (params[:order] == "down") ? " DESC" :""
      else
        if session[:"order_#{model_name}"]
          order = session[:"order_#{model_name}"]
        end
      end
      session[:"order_#{model_name}"] = order
      order
    end
    
    def apply_block_filters
      apply_filter(:all)
      filter_name = session[:"#{model_name}_filter"]
      apply_filter(filter_name) if filter_name != :all
    end

    def apply_filter(filter_name)
      if filter = self.class.get_filter(filter_name)
        if filter.block
          @list.reject! do |r| 
            !filter.block.call(r)
          end
        elsif filter.method
          @list.reject! do |r|
            !send(filter.method, r)
          end
        end
      end
    end
    
    def create_paginator
      per_page = get_per_page
      page = params[:no_paginate] ? 1 : get_page
      offset = (page - 1) * per_page
      if @list
        count = @list.size
        set_model_list @list[offset..(offset + per_page - 1)] || []
        pag = ActionController::Pagination::Paginator.new(self, count, per_page, page)
        instance_variable_set("@#{model_list_name}_paginator", pag)
      else
        set_model_list []
      end
    end
    
    def get_page
      params[:page] ||= session[:"#{model_name}_page"] || 1
      session[:"#{model_name}_page"] = params[:page].to_i
    end
    
    def get_per_page
      params[:no_paginate] ? 99999 : (self.class.options[:per_page] || 10)
    end

    def get_current
      begin
        set_model model_class.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        flash[:error] = "No existe el elemento solicitado."
        redirect_to :controller => model_list_name, :action => 'index'
        return false
      end
    end
    
    def get_parent
      if parent = options[:parent]
        begin
          @parent = instance_variable_set("@#{parent}", parent_class.find(params[:"#{parent}_id"]))
        rescue ActiveRecord::RecordNotFound
          flash[:error] = "No existe el padre del elemento solicitado"
          redirect_to ''
          return false
        end
      end
    end

    def parent_key
      options[:foreign_key] || "#{options[:parent]}_id".to_sym
    end
    
    
    @@optional_callbacks = [:before_assign, :before_save, :after_save, :before_render]
    def method_missing_with_callbacks(method, *args)
      return if @@optional_callbacks.include?(method)
      method_missing_without_callbacks(method, args)
    end
    alias_method_chain :method_missing, :callbacks
  end
end